# # 错误，调试和测试
#
# # 1. 错误处理
# # try...except...finally...
# # 所有的错误类型都继承自BaseException，所以在使用except时需要注意的是，它不但捕获该类型的错误，还把其子类也“一网打尽”
# def fun1(num):
#     try:
#         print('try...')
#         r = 10 / int(num)
#         print('result:', r)
#     except ValueError as e:
#         print('ValueError:', e)
#     except ZeroDivisionError as e:
#         print('except:', e)
#     finally:
#         print('finally...')
#     print('end')
#
#
# fun1(2)
# fun1(0)
# fun1('0')
# fun1('1')
# fun1('abc')
#
#
# # 使用try...except捕获错误还有一个巨大的好处，就是可以跨越多层调用，
# # 比如函数main()调用foo()，foo()调用bar()，结果bar()出错了，这时，只要main()捕获到了，就可以处理：
# # 也就是说，不需要在每个可能出错的地方去捕获错误，只要在合适的层次去捕获错误就可以了。
# # 这样一来，就大大减少了写try...except...finally的麻烦
# def foo(s):
#     return 10 / int(s)
#
#
# def bar(s):
#     return foo(s) * 2
#
#
# def main():
#     print('-----main()-------')
#     try:
#         bar('0')
#     except Exception as e:
#         print('Error:', e)
#     finally:
#         print('finally...')
#     print('end...')
#
#
# def main2():
#     print('-----main2222()-------')
#     bar('0')
#     print('end...')
#
#
# main()
# # main2()
#
#
# # 记录错误
# # 如果不捕获错误，自然可以让Python解释器来打印出错误堆栈，但程序也被结束了。既然我们能捕获错误，就可以把错误堆栈打印出来，然后分析错误原因，同时，让程序继续执行下去。
# # Python内置的logging模块可以非常容易地记录错误信息：
# import logging
#
#
# def foo(s):
#     return 10 / int(s)
#
#
# def bar(s):
#     return foo(s) * 2
#
#
# def main():
#     print('-----logging----')
#     try:
#         bar('0')
#     except Exception as e:
#         logging.exception(e)
#     finally:
#         print('finally...')
#     print('end...')
#
#
# # main()
# # print('main end...')
#
#
# # 抛出错误
# # 因为错误是class，捕获一个错误就是捕获到该class的一个实例。因此，错误并不是凭空产生的，而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误，我们自己编写的函数也可以抛出错误。
# # 如果要抛出错误，首先根据需要，可以定义一个错误的class，选择好继承关系，然后，用raise语句抛出一个错误的实例：
# class FooError(ValueError):
#     pass
#
#
# def foo(s):
#     n = int(s)
#     if n == 0:
#         raise FooError('invalid value:%s' % s)
#     return 10 / n
#
#
# print('---- 抛出错误 -----')
# try:
#     foo('0')
# except FooError as e:
#     print('FooError:', e)
#
#
# # err_reraise.py
#
# def foo(s):
#     n = int(s)
#     if n == 0:
#         raise ValueError('invalid value: %s' % s)
#     return 10 / n
#
#
# def bar():
#     try:
#         foo('0')
#     except ValueError as e:
#         print('ValueError!')
#         raise
#
#
# # bar()
#
#
# # 在bar()函数中，我们明明已经捕获了错误，但是，打印一个ValueError!后，又把错误通过raise语句抛出去了，这不有病么？
# # 其实这种错误处理方式不但没病，而且相当常见。捕获错误目的只是记录一下，便于后续追踪。但是，由于当前函数不知道应该怎么处理该错误，所以，最恰当的方式是继续往上抛，让顶层调用者去处理。好比一个员工处理不了一个问题时，就把问题抛给他的老板，如果他的老板也处理不了，就一直往上抛，最终会抛给CEO去处理。
#
#
# # 2. 调试
# # 第一种方法简单直接粗暴有效，就是用print()把可能有问题的变量打印出来看看
# # print()最大的坏处是将来还得删掉它，想想程序里到处都是print()，运行结果也会包含很多垃圾信息
# # 第二种方法：断言
# def foo(s):
#     n = int(s)
#     assert n != 0, 'n is zero!'
#     return 10 / n
#
#
# def main():
#     print('--- 调试.断言 ---')
#     foo('0')
#
#
# # main()
#
# # 程序中如果到处充斥着assert，和print()相比也好不到哪去。不过，启动Python解释器时可以用-O参数来关闭assert：
# # $ python -O err.py
#
# # 第3种方式：logging， 和assert比，logging不会抛出错误，而且可以输出到文件
# import logging
#
# logging.basicConfig(level=logging.INFO)
# s = '0'
# n = int(s)
# print('logging.info。。。')
# logging.info('m=%d' % n)
# # print(10 / n)
#
# # 第4种方式: 启动Python的调试器pdb，让程序以单步方式运行，可以随时查看运行状态。
# # 1.python -m pdb demo08.py
# # 2.以参数-m pdb启动后，pdb定位到下一步要执行的代码-> s = '0'。输入命令l来查看代码：l
# # 3.输入命令n可以单步执行代码
# # 4.任何时候都可以输入命令p 变量名来查看变量：
# # 5.输入命令q结束调试，退出程序
#
# # pdb.set_trace()
# # 这个方法也是用pdb，但是不需要单步执行，我们只需要import pdb，然后，在可能出错的地方放一个pdb.set_trace()，就可以设置一个断点
# import pdb
#
# print('hihihihi')
# pdb.set_trace()
# print('hahaha')
#
#
# # 运行代码，程序会自动在pdb.set_trace()暂停并进入pdb调试环境，可以用命令p查看变量，或者用命令c继续运行
#
#
# 3. 单元测试
# 如果你听说过“测试驱动开发”（TDD：Test-Driven Development），单元测试就不陌生。
# 如果单元测试通过，说明我们测试的这个函数能够正常工作。如果单元测试不通过，要么函数有bug，要么测试条件输入不正确，总之，需要修复使单元测试能够通过。
# 单元测试通过后有什么意义呢？如果我们对abs()函数代码做了修改，只需要再跑一遍单元测试，如果通过，说明我们的修改不会对abs()函数原有的行为造成影响，如果测试不通过，说明我们的修改与原有行为不一致，要么修改代码，要么修改测试。
# 这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候，可以极大程度地保证该模块行为仍然是正确的
class Dict(dict):
    def __init__(self, **kw):
        super().__init__(self, **kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value


# 为了编写单元测试，我们需要引入Python自带的unittest模块，编写mydict_test.py如下
# 编写单元测试时，我们需要编写一个测试类，从unittest.TestCase继承。
# 以test开头的方法就是测试方法，不以test开头的方法不被认为是测试方法，测试的时候不会被执行。
# 对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断，我们只需要调用这些方法就可以断言输出是否是我们所期望的
import unittest
from basic_demo.demo08 import Dict


class TestDict(unittest.TestCase):
    def test_init(self):
        d = Dict(a=1, b='test')
        # 最常用的断言就是assertEqual()：
        self.assertEqual(d.a, 1)
        self.assertEqual(d.b, 'test')
        self.assertTrue(isinstance(d, dict))

    def test_key(self):
        d = Dict()
        d['key'] = 'value'
        self.assertEqual(d.key, 'value')

    def test_attr(self):
        d = Dict()
        d.key = 'value'
        self.assertTrue('key' in d)
        self.assertEqual(d['key'], 'value')

    def test_keyerror(self):
        d = Dict()
        # 另一种重要的断言就是期待抛出指定类型的Error，比如通过d['empty']访问不存在的key时，断言会抛出KeyError：
        with self.assertRaises(KeyError):
            value = d['empty']

    def test_attrerror(self):
        d = Dict()
        with self.assertRaises(AttributeError):
            value = d.empty

    def setUp(self):
        print('setUp...')

    def tearDown(self):
        print('tearDown...')


# if __name__ == '__main__':
#     unittest.main()
# python -m unittest mydict_test

# setUp与tearDown
# 可以在单元测试中编写两个特殊的setUp()和tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
# setUp()和tearDown()方法有什么用呢？设想你的测试需要启动一个数据库，这时，就可以在setUp()方法中连接数据库，在tearDown()方法中关闭数据库，这样，不必在每个测试方法中重复相同的代码：


# 文档测试
# 如果你经常阅读Python的官方文档，可以看到很多文档都有示例代码。比如re模块就带了很多示例代码：
import re

m = re.search('(?<=abc)def', 'abcdef')
print(m.group(0))


class Dict(dict):
    '''
    Simple dict but also support access as x.y style.

    >>> d1 = Dict()
    >>> d1['x'] = 100
    >>> d1.x
    100
    >>> d1.y = 200
    >>> d1['y']
    200
    >>> d2 = Dict(a=1, b=2, c='3')
    >>> d2.c
    '3'
    >>> d2['empty']
    Traceback (most recent call last):
        ...
    KeyError: 'empty'
    >>> d2.empty
    Traceback (most recent call last):
        ...
    AttributeError: 'Dict' object has no attribute 'empty'
    '''

    def __init__(self, **kw):
        super(Dict, self).__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value


if __name__ == '__main__':
    import doctest
    print('----doctest-----')
    doctest.testmod()
# 什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题，比如把__getattr__()方法注释掉，再运行就会报错：
